using System;
using System.IO;
using System.Data;
using System.Data.SqlClient;
using System.Text;
using System.Text.RegularExpressions;
using System.Security;
using System.Security.Permissions;
using System.Collections;
using VisualStudioSensor;

namespace TeamFoundationServerSensor {

    /// <summary>
    /// A class which loads and sets files. We need variables to be static so
    /// that if using a file we don't need to worry about inconsistencies
    /// between stored values.
    /// </summary>
    public class Settings {

        /// <summary>
        /// This class makes use of the Singleton design pattern to allow for
        /// efficient setting loading and synchronization.
        /// </summary>
        public static readonly Settings Instance = new Settings();

        #region Fields and Constructors
        /// <summary>
        /// A hash table to store values if we are loading from a file. This
        /// is done so we don't have to read the file multiple times each time
        /// we want to get a setting.
        /// </summary>
        private Hashtable settings;

        /// <summary>
        /// Used to indicate that the singelton Instance have been instantiated.
        /// </summary>
        private Boolean initiated = false;

        /// <summary>
        /// The path to the setting file that was loaded.
        /// </summary>
        private string settingFile;

        /// <summary>
        /// Constructor
        /// </summary>
        private Settings() {

            // This code should only run the first time this class is created.
            if (initiated == false) {

                settings = new Hashtable();
                settingFile = "./settings.ini";

                initiated = true;
            }
        }
        #endregion

        #region Get SettingVisitor

        /// <summary>
        /// Attempt to get a setting which is known to be Boolean.
        /// </summary>
        /// <param name="setting">The setting which we wish to retrieve.</param>
        /// <returns>True if setting is true, False otherwise.</returns>
        public Boolean getSettingBoolean(string setting) {

            try {
                return Boolean.Parse(getSetting(setting));
            }
            catch (Exception e) {
                Logger.TheInstance.Log("Could not retrieve Boolean setting. " 
                    + e, LogLevel.Error);
            }

            return false;
        }

        /// <summary>
        /// Method that retrieves the appropriate value from a file or setting.
        /// </summary>
        /// <param name="setting">The key of the setting file we wish to return.</param>
        /// <returns>String value if possible or null otherwise.</returns>
        public string getSetting(string setting) {

            // Default value if setting does not exist.
            string value = null;

            // Check parameters for validity
            if (setting == null || setting.Equals(String.Empty)) {
                Logger.TheInstance.Log("Invalid setting '" + setting + "'", LogLevel.Error);
            }
            else {

                // Determine whether to use a file or database.
                if (Settings.Instance.UseFile) {
                    value = getSettingFile(setting);
                }
                else {
                    value = getSettingServer(setting);
                }
            }

            return value;
        }

        /// <summary>
        /// Return the specified property value from the setting file.
        /// </summary>
        /// <param name="setting">The key of the setting file we wish to return.</param>
        /// <returns>String value from the server.</returns>
        private string getSettingFile(string setting) {

            // Return value of the setting.
            string value = null;

            // Very easy since we are simply working with the hashtable
            if (settings.Contains(setting)) {
                value = (string)settings[setting];
            }

            // Return a nice error message because we couldn't read the setting.
            else {
                Logger.TheInstance.Log("Error, could not read setting from file: "
                    + setting, LogLevel.Error);
            }

            return value;
        }

        /// <summary>
        /// Return the specified property value from the server.
        /// </summary>
        /// <param name="setting">The key of the setting file we wish to return.</param>
        /// <returns>String value from the server.</returns>
        private string getSettingServer(string setting) {

            // Return value of the setting.
            string value = null;

            // Create a new connection object.
            SqlConnection conn = Connections.getNewSQLConnection(Settings.Instance.Database);

            // Create query to check for database.
            string query = @"SELECT value FROM settings WHERE setting='" + setting + "'";
            SqlCommand command = new SqlCommand(query, conn);

            try {

                // Open connection and execute query.
                conn.Open();
                SqlDataReader rows = command.ExecuteReader();

                // Found the values.
                if (rows.Read()) {

                    // There should only be one value.
                    value = (string)rows["value"];
                    Logger.TheInstance.Log("Found value: " + value, LogLevel.Info);
                    rows.Close();
                }

                // Could not find setting.
                else {
                    Logger.TheInstance.Log("SettingVisitor does not exist: "
                        + setting, LogLevel.Info);
                }

                rows.Close();
            }
            catch (Exception e) {
                Logger.TheInstance.Log("Error while trying to access setting: "
                    + setting + " " + e, LogLevel.Error);
            }
            finally {
                conn.Close();
            }

            // Return a nice error message because we couldn't read the setting.
            if (setting == null) {
                Logger.TheInstance.Log("Error, could not read setting from server: "
                    + setting, LogLevel.Error);
            }

            return value;
        }
        #endregion

        #region Save SettingVisitor

        /// <summary>
        /// Public method used for saving all settings
        /// </summary>
        /// <returns>Returns true if value could be stored. </returns>
        public Boolean save() {

            Boolean status = false;

            // Save to file
            if (Settings.Instance.UseFile) {
                   status = saveFile();
            }

            // Save to database
            else {
                IDictionaryEnumerator enumerator = settings.GetEnumerator();
                while (enumerator.MoveNext()) {
                    saveSettingServer((string)enumerator.Key, (string)enumerator.Value);
                }
                status = true;
            }

            return status;
        }

        /// <summary>
        /// Public method used for storing Key:value pair settings.
        /// </summary>
        /// <param name="setting">The key that we will store the value with.</param>
        /// <param name="value">The value associated with the key.</param>
        /// <returns>Returns true if value could be stored. </returns>
        public Boolean saveSetting(string setting, string value) {

            // Check parameters for validity
            if (setting == null || setting.Equals(String.Empty)) {
                Logger.TheInstance.Log("Invalid setting '" + setting
                + "' with value '" + value + "'", LogLevel.Error);
            }
            else {

                if (setting.Equals("UseFile")) {
                    saveSettingFile(setting, value);
                }

                // Check whether to use a file or database.
                if (Settings.Instance.UseFile) {
                    return saveSettingFile(setting, value);
                }
                else {
                    return saveSettingServer(setting, value);
                }
            }

            return false;
        }

        /// <summary>
        /// A public method used for storing Key:value pair settings in the
        /// settings file.
        /// </summary>
        /// <param name="setting">The key that we will store the value with.</param>
        /// <param name="value">The value associated with the key.</param>
        /// <returns>Returns true if value could be stored. </returns>
        private Boolean saveSettingFile(string setting, string value) {

            // Remove existing setting if applicable.
            if (settings.Contains(setting)) {
                settings.Remove(setting);
            }

            // Very easy to add or update setting to hashtable.
            settings.Add(setting, value);

            return true;
        }

        /// <summary>
        /// A public method used for storing Key:value pair settings in the SQL 
        /// database.
        /// </summary>
        /// <param name="setting">The key that we will store the value with.</param>
        /// <param name="value">The value associated with the key.</param>
        /// <returns>Returns true if value could be stored. </returns>
        private Boolean saveSettingServer(string setting, string value) {

            // Variable to store true or false settings.
            Boolean success = false;

            // We can attempt to store value in database.
            if (checkCreateDatabase() && checkCreateSettingsTable()) {

                // Create a new connection object.
                SqlConnection conn = Connections.getNewSQLConnection(Settings.Instance.Database);

                // Create query to add or update key/value
                string checkQuery = "SELECT * FROM settings WHERE setting='" + setting + "'";
                string insertQuery = "INSERT INTO settings (setting, value) VALUES('" + setting + "','" + value + "')";
                string updateQuery = "UPDATE settings SET value='" + value + "' WHERE setting='" + setting + "'";

                SqlCommand cCheck = new SqlCommand(checkQuery, conn);
                SqlCommand cInsert = new SqlCommand(insertQuery, conn);
                SqlCommand cUpdate = new SqlCommand(updateQuery, conn);

                try {

                    conn.Open();
                    SqlDataReader rowsAffected = cCheck.ExecuteReader();
                    Boolean hasRows = rowsAffected.HasRows;
                    rowsAffected.Close();

                    // Update query
                    if (hasRows) {
                        if (cUpdate.ExecuteNonQuery() > 0) {
                            success = true;
                            Logger.TheInstance.Log("Updated setting '" + setting + "' with value '"
                                + value + "'", LogLevel.Info);
                        }
                    }

                    // Insery Query
                    else {
                        if (cInsert.ExecuteNonQuery() > 0) {
                            success = true;
                            Logger.TheInstance.Log("Inserted setting '" + setting + "' with value '"
                            + value + "'", LogLevel.Info);
                        }
                    }
                }
                catch (Exception e) {
                    Logger.TheInstance.Log("Error while storing value: " + e, LogLevel.Error);
                }
                finally {
                    conn.Close();
                }
            }

            // Print different log messages based on success or failure of query.
            if (!success) {
                Logger.TheInstance.Log("Could not save setting '" + setting
                    + "' with value '" + value + "'", LogLevel.Error);
            }

            return success;
        }
        #endregion

        #region Helper Methods
        /// <summary>
        /// Determine if the SQL database Hackystat exists.
        /// </summary>
        /// <returns>True if the database exists or could be created.</returns>
        private Boolean checkCreateDatabase() {

            Boolean success = false;

            // Create a new connection object.
            SqlConnection conn = Connections.getNewSQLConnection("master");

            // Create query to check for database.
            string query = "SELECT * FROM master.dbo.sysdatabases WHERE NAME='"
                + Settings.Instance.Database + "'";
            SqlCommand command = new SqlCommand(query, conn);

            try {

                // Open connection and execute query.
                conn.Open();
                SqlDataReader rowsAffected = command.ExecuteReader();

                // Database exists and it is ok to proceed.
                if (rowsAffected.HasRows) {
                    success = true;
                    Logger.TheInstance.Log("Database exists: " 
                        + Settings.Instance.Database, LogLevel.Trace);
                    rowsAffected.Close();
                }

                // Database does not exist so we should try and create it.
                else {
                    string createQuery = "USE master CREATE DATABASE " + Settings.Instance.Database
                           + " ON PRIMARY"
                           + " (NAME = Hackystat,"
                           + @" FILENAME = 'c:\program files\microsoft sql server\mssql.1\mssql\data\" + Settings.Instance.Database + ".mdf',"
                           + " SIZE = 4,"
                           + " MAXSIZE = 10,"
                           + " FILEGROWTH = 5 )"
                           + " LOG ON"
                           + " (NAME = 'Hackystat_log',"
                           + @" FILENAME = 'c:\program files\microsoft sql server\mssql.1\mssql\data\" + Settings.Instance.Database + "log.ldf',"
                           + " SIZE = 5MB,"
                           + " MAXSIZE = 25MB,"
                           + " FILEGROWTH = 5MB )";


                    // Attempt to create database;
                    SqlCommand createCommand = new SqlCommand(createQuery, conn);
                    rowsAffected.Close();

                    // Determine if database exists correctly.
                    if (createCommand.ExecuteNonQuery() == -1) {
                        success = true;
                        Logger.TheInstance.Log("Created database: " + Settings.Instance.Database, LogLevel.Trace);
                    }
                }
            }
            catch (Exception e) {
                Logger.TheInstance.Log("Error while creating database: " + e, LogLevel.Error);
            }
            finally {
                conn.Close();
            }

            if (!success) Logger.TheInstance.Log("Could not connect or create database: " + Settings.Instance.Database, LogLevel.Error);
            return success;
        }

        /// <summary>
        /// Checks to see if the settings table exists and if not attempts to 
        /// create it.
        /// </summary>
        /// <returns>Returns true if table exists or can be created.</returns>
        private Boolean checkCreateSettingsTable() {

            Boolean success = false;

            // Create a new connection object.
            SqlConnection conn = Connections.getNewSQLConnection(Settings.Instance.Database);

            // Check if table exists
            string existQuery = "SELECT name FROM sysObjects WHERE name like 'settings'";
            string createTableQuery = "CREATE TABLE settings ("
                + " setting varchar(50) NOT NULL,"
                + " value varchar(MAX) NOT NULL,"
                + " PRIMARY KEY (setting) )";

            SqlCommand queryExist = new SqlCommand(existQuery, conn);
            SqlCommand createTable = new SqlCommand(createTableQuery, conn);

            try {

                // Open connection and execute query.
                conn.Open();
                SqlDataReader result = queryExist.ExecuteReader();

                // Table exists and it is ok to proceed.
                if (result.HasRows) {
                    success = true;
                    Logger.TheInstance.Log("Table exists: settings", LogLevel.Trace);
                    result.Close();
                }
                else {

                    // Try to insert table.
                    result.Close();
                    if (createTable.ExecuteNonQuery() == -1) {
                        success = true;
                        Logger.TheInstance.Log("Created table: settings", LogLevel.Trace);
                    }
                }
            }
            catch (Exception e) {
                Logger.TheInstance.Log("Error while creating table: " + e, LogLevel.Error);
            }
            finally {
                conn.Close();
            }

            if (!success) Logger.TheInstance.Log("Could not access or create table: settings", LogLevel.Error);
            return success;
        }

        /// <summary>
        /// Attempts to read the setting file.
        /// </summary>
        /// <param name="settingPath">The path to a settings file.</param>
        /// <returns>True if the file could be loaded.</returns>
        public Boolean loadFile(string settingPath) {

            Boolean success = false;

            // Determine if the file exists or if we need to create it.
            FileStream file = null;
            StreamReader fileReader = null;
            try {

                // Check if the setting file exists.
                if (!File.Exists(settingPath)) {
                    throw new FileLoadException("Could not load the file: " + settingPath);
                }

                // Check security for reading the file.
                FileIOPermission securitycheck = new FileIOPermission(FileIOPermissionAccess.Read, settingPath);
                securitycheck.Demand();

                // Attempt to open the file and create a hashtable.
                file = new FileStream(settingPath, FileMode.OpenOrCreate, FileAccess.Read);
                fileReader = new StreamReader(file);
                settings = new Hashtable();

                // Read from the file line-by-line
                string temp = fileReader.ReadLine();
                while (temp != null) {

                    // Trim whitespace
                    temp.Trim();

                    if (temp.Length != 0 && temp.IndexOf('#') != 0) {
                        if (temp.IndexOf('=') >= 1) {
                            string[] tempKeyValue = temp.Split('=');
                            settings.Add(tempKeyValue[0], tempKeyValue[1]);
                        }
                        else {
                            Logger.TheInstance.Log("Malformed line: " + temp, LogLevel.Error);
                        }
                    }
                    else {
                        Logger.TheInstance.Log("Skip comment line: " + temp, LogLevel.Trace);
                    }

                    temp = fileReader.ReadLine();
                }

                // Check to make sure all the settings are correct.
                if (checkSettings()) {
                    settingFile = settingPath;
                }

                success = true;
            }
            catch (FileLoadException e) {
                Logger.TheInstance.Log(e);
            }
            catch (SecurityException e) {
                Logger.TheInstance.Log(e);
            }
            catch (Exception e) {
                Logger.TheInstance.Log(e);
            }
            finally {
                // Cleanup
                if (fileReader != null) fileReader.Close();
                if (file != null) file.Close();
            }

            return success;
        }

        /// <summary>
        /// Checks the settings in the hashtable to verify they are correct and
        /// that no critical settings are missing.
        /// </summary>
        /// <returns>True if all settings are correct, false otherwise.</returns>
        private Boolean checkSettings() {
            
            try {
                checkDefaultSetting("UseFile", "true");

                checkDefaultSetting("UseIEvent", "true");
                checkDefaultSetting("BisSubscribe", @"%PROGRAMFILES%\Microsoft Visual Studio 2005 Team Foundation Server\TF Setup\BisSubscribe.exe");

                checkDefaultSetting("UseWindowsSQLLogin", "true");
                checkDefaultSetting("TFSUser", "guest");
                checkDefaultSetting("TFSPassword", "");

                checkDefaultSetting("Server", "localhost");
                checkDefaultSetting("Address", "http://localhost:7777/Hackystat_VSTFS_Sensor/Services/");
                checkDefaultSetting("Database", "Hackystat");

                // Optional values
                checkDefaultSetting("SensorPath", ".");
                checkDefaultSetting("QuitTimeout", "5000");
                checkDefaultSetting("Sensorbase", "");
            }
            catch (Exception e) {
                Logger.TheInstance.Log("Could not verify all settings. ", LogLevel.Error);
                Logger.TheInstance.Log(e);
                return false;
            }

            return true;            
        }

        /// <summary>
        /// If setting does not exist in the hashmap then create it with
        /// default setting of value.
        /// </summary>
        /// <param name="key">The setting to check.</param>
        /// <param name="value">The value to use if it does not exist.</param>
        private void checkDefaultSetting(string key, string value) {
            if (!settings.Contains(key)) {
                settings.Add(key, value);
            }
        }

        /// <summary>
        /// Attempts to save the settings stored in the hashtable.
        /// </summary>
        /// <returns>True if settings could be saved.</returns>
        private Boolean saveFile() {

            Boolean success = false;

            // Create a copy of the hashtable for editing.
            Hashtable oldsettings = (Hashtable)settings.Clone();

            // Attempt to open the settings file for writing.
            FileStream file = null;
            StreamReader fileReader = null;
            StreamWriter fileWriter = null;
            MemoryStream memStream = new MemoryStream();
            ASCIIEncoding encoder = new ASCIIEncoding();

            try {

                // Check security for reading the file.
                FileIOPermission securitycheck = new FileIOPermission(FileIOPermissionAccess.Read 
                    & FileIOPermissionAccess.Write, settingFile);
                securitycheck.Demand();


                // Attempt to open the file and create a hashtable.
                file = new FileStream(settingFile, FileMode.OpenOrCreate, FileAccess.ReadWrite);
                fileReader = new StreamReader(file);
                fileWriter = new StreamWriter(memStream);

                // Look for and overwrite existing settings.
                string temp = fileReader.ReadLine();
                while (temp != null) {

                    // Trim whitespace
                    temp.Trim();

                    // Determine what each line is made of.
                    if (temp.Length != 0 && temp.IndexOf('#') != 0) {
                        if (temp.IndexOf('=') >= 1) {

                            // Read the value currently in the text.
                            string[] tempKeyValue = temp.Split('=');

                            // Create and replace the new line.
                            temp = tempKeyValue[0] + "=" + oldsettings[tempKeyValue[0]] + System.Environment.NewLine;
                            memStream.Write(encoder.GetBytes(temp), 0, encoder.GetByteCount(temp));

                            // Remove the setting from the temp hash.
                            oldsettings.Remove(tempKeyValue[0]);
                        }
                        else {
                            Logger.TheInstance.Log("Removing malformed line: " + temp, LogLevel.Error);
                        }
                    }

                    // Found a comment or blank line.
                    else {
                        temp = temp + System.Environment.NewLine;
                        memStream.Write(encoder.GetBytes(temp), 0, encoder.GetByteCount(temp));
                    }

                    temp = fileReader.ReadLine();
                }

                // Add any new settings that do not exist.
                IDictionaryEnumerator enumerator = oldsettings.GetEnumerator();
                while (enumerator.MoveNext()) {
                    string line = enumerator.Key + "=" + enumerator.Value + System.Environment.NewLine;
                    memStream.Write(encoder.GetBytes(line), 0, encoder.GetByteCount(line));
                }

                // Close the current settings file
                fileReader.Close();

                // Rename current setting file.
                FileInfo oldFile = new FileInfo(settingFile);
                string oldExtension = ".backup";
                if (File.Exists(settingFile + oldExtension)) {
                    File.Delete(settingFile + oldExtension);
                }
                oldFile.MoveTo(settingFile + oldExtension);

                // Replace current setting file.
                file = new FileStream(settingFile, FileMode.OpenOrCreate, FileAccess.Write);
                fileWriter = new StreamWriter(file);
                memStream.WriteTo(fileWriter.BaseStream);

                success = true;
            }
            catch (FileLoadException e) {
                Logger.TheInstance.Log(e);
            }
            catch (SecurityException e) {
                Logger.TheInstance.Log(e);
            }
            catch (IOException e) {
                Logger.TheInstance.Log(e);
            }
            catch (UnauthorizedAccessException e) {
                Logger.TheInstance.Log(e);
            }
            finally {
                // Cleanup
                if (fileReader != null) fileReader.Close();
                if (fileWriter != null) fileWriter.Close();
                if (file != null) file.Close();
            }

            return success;
        }

        #endregion

        #region Get and Set Methods

        /// <summary>
        /// Gets the name of the default database that will be used if the
        /// setting usefile is false.
        /// </summary>
        /// <returns>The name of the database.</returns>
        public string Database {
            get { return (string)settings["Database"]; }
        }

        /// <summary>
        /// Can be used to determine whether a file is being used to store
        /// settings.
        /// </summary>
        /// <returns>The value of the setting usefile.</returns>
        public Boolean UseFile {
            get {
                try {
                    return Boolean.Parse((string)settings["UseFile"]);
                }
                catch (Exception e) {
                    Logger.TheInstance.Log("Strange, cannot find UseFile setting " + e, LogLevel.Error);
                    return true;
                }
            }
        }

        #endregion
    }
}